package se.webbzon.boltzmann.game.object.camera;

import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import se.webbzon.boltzmann.camera.AbstractCamera;
import se.webbzon.boltzmann.input.Button;
import se.webbzon.boltzmann.input.Input;
import se.webbzon.boltzmann.input.InputListener;
import se.webbzon.boltzmann.player.Player;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldobject.WorldObject;
import se.webbzon.oschi01.worldobject.WorldStaticObject;

public class GameCamera extends AbstractCamera {
	
	// The default hover height and depth of the camera when no input is applied
	private static final double defaultHeight = 64;
	private static final double defaultDepth = 320;
	private static final double adjustmentSpeed = 0.04;
	
	// The movable range when an input is applied
	private static final double range = 128;
	
	// A vector pointing upwards
	private static final Vector3d up = new Vector3d(0,1,0);
	
	// The player associated with this game camera
	private final Player player;
	
	// The input listener associated with this game camera
	private final InputListener inputListener;
	
	// The camera position and the target position
	private final Point3d to, offset, preferredTo, eye, center;
	
	// The preferred hover height and depth of the camera and the
	// actual hover height and depth of the camera
	private double preferredHeight, preferredDepth, height, depth;
	
	// The target of the camera
	private WorldStaticObject target;
	
	// The direction and value of the secondary axis
	private volatile double direction, value;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new game camera associated to a given player. 
	 * The game camera will focus onto the given point. **/
	public GameCamera(Player player, Point3d target) {
		this(player);
		setTarget(target.x,target.y,target.z);
	}
	
	/** Creates a new game camera associated to a given player. 
	 * The game camera will focus onto the given static world object. **/
	public GameCamera(Player player, WorldStaticObject target) {
		this(player);
		setTarget(target);
		
	}
	
	/** Creates a new game camera associated to a given player. 
	 * The game camera will focus onto the origin of the world. **/
	public GameCamera(Player player) {
		super(WorldObject.onCreate + WorldObject.onStepEnd + WorldObject.onDestroy);
		this.player = player;
		this.inputListener = new InputListenerImpl();
		preferredDepth = depth = defaultDepth;
		preferredHeight = height = defaultHeight;
		to = new Point3d();
		preferredTo = new Point3d();
		offset = new Point3d(0,16,0);
		eye = new Point3d();
		center = new Point3d();
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Returns the player controlling the game camera. **/
	public Player getPlayer() {
		return player;
	}
	
	/** Copies the position of the eye to the given point3d. **/
	public void getEye(Point3d eye) {
		eye.set(this.eye);
	}
	
	/** Copies the position of the center to the given point3d. **/
	public void getCenter(Point3d center) {
		center.set(this.center);
	}
	
	/** Returns the x-spacing between the center and the eye of the 
	 * game camera. The spacing is calculated as 
	 * center.getX() - eye.getX().**/
	public double getXSpacing() {
		return center.x - eye.x;
	}
	
	/** Returns the y-spacing between the center and the eye of the 
	 * game camera. The spacing is calculated as 
	 * center.getY() - eye.getY().**/
	public double getYSpacing() {
		return center.y - eye.y;
	}
	
	/** Returns the z-spacing between the center and the eye of the 
	 * game camera. The spacing is calculated as 
	 * center.getZ() - eye.getZ().**/
	public double getZSpacing() {
		return center.z - eye.z;
	}
	
	/** Sets the preferred hover height and depth for the camera. **/
	public void setHover(double height, double depth) {
		preferredHeight = height;
		preferredDepth = depth;
	}
	
	/** Returns the preferred hover height for the camera. **/
	public double getHoverHeight() {
		return preferredHeight;
	}
	
	/** Returns the preferred hover depth for the camera. **/
	public double getHoverDepth() {
		return preferredDepth;
	}
	
	/** Sets the camera offset relative to the target. **/
	public void setOffset(double x, double y, double z) {
		offset.set(x,y,z);
	}
	
	/** Returns the x-offset of the camera relative to the target. **/
	public double getXOffset() {
		return offset.getX();
	}
	
	/** Returns the y-offset of the camera relative to the target. **/
	public double getYOffset() {
		return offset.getY();
	}
	
	/** Returns the z-offset of the camera relative to the target. **/
	public double getZOffset() {
		return offset.getZ();
	}
	
	/** Focuses the target onto the given instance. The camera will follow
	 * the instance around in the world until this method is called with
	 * the value null. **/
	public void setTarget(WorldStaticObject instance) {
		target = instance;
		if (instance != null)
			setTarget(instance.getX(),instance.getY(),instance.getZ());
	}
	
	/** Returns the target which the camera has focused onto. If no instance
	 * has focus then null is returned. **/
	public WorldStaticObject getTarget() {
		return target;
	}
	
	/** Sets the target of the camera to the given position. This method
	 * will only be applied if the targeted instance is null. **/
	public void setTarget(double x, double y, double z) {
		if (target == null)
			preferredTo.set(x,y,z);
	}
	
	/** Resets the hover distance of the camera. **/
	public void resetHover() {
		preferredHeight = defaultHeight;
		preferredDepth = defaultDepth;
	}
	
	/** Snaps the camera to the given position without delay. **/
	public void snap() {
		to.set(preferredTo);
		height = preferredHeight;
		depth = preferredDepth;
	}
	
	@Override public void onCreate(WorldArea world) {
		player.getInput().addListener(inputListener);
	}
	
	@Override public void onStepEnd(WorldArea world) {
		// Interpolate the depth
		if (Math.abs(depth - preferredDepth) < 1)
			depth = preferredDepth;
		else
			depth += adjustmentSpeed * (preferredDepth - depth);
		
		// Interpolate the height
		if (Math.abs(height - preferredHeight) < 1)
			height = preferredHeight;
		else
			height += adjustmentSpeed * (preferredHeight - height);
		
		// Retrieve the preferred target 
		if (target != null)
			target.getPosition(preferredTo);
		
		// Interpolate the target position
		to.interpolate(to, preferredTo, 4 * adjustmentSpeed);
		
		// Calculate the center and the eye of the camera
		eye.set(offset.x + to.x + value * Math.cos(direction) * range,
				offset.y + to.y + height,
				offset.z + to.z + depth - range * value * Math.sin(direction));
		center.set(offset.x + to.x,offset.y + to.y,offset.z + to.z);
		
		// Set the camera
		setProjectionPerspective(eye, center, up);
	}
	
	@Override public void onDestroy(WorldArea world) {
		player.getInput().removeListener(inputListener);
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	/** An implementation of an InputListener. **/
	private final class InputListenerImpl implements InputListener {
		
		@Override public void primaryAxisChanged(Input input, double direction, double value) {};

		@Override public void secondaryAxisChanged(Input input, double direction, double value) {
			GameCamera.this.direction = direction;
			GameCamera.this.value = value;
		}

		@Override public void buttonPressed(Input input, Button button) {};

		@Override public void buttonReleased(Input input, Button button) {};
		
		@Override public void bindingAccepted(Input input, Button button) {};
		
	}
	
}
